Last active
May 20, 2024 14:11
-
-
Save bigodel/15599f3c1da23d1008b7d7d4ff8bff77 to your computer and use it in GitHub Desktop.
Literate Ledger Programming(?) with Org-mode
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #+title: Literate Ledger Programming(?) with Org-mode | |
| #+roam_tags: org-mode ledger | |
| Recently, given the economic hardships presented by the current pandemic, | |
| coupled with the fact that I have started making my own money, I have started to | |
| keep track of my finances. I have always known that I would use [[https://www.ledger-cli.org/][Ledger]], the | |
| command line account tool, to manage my money. Ledger was created by an Emacs | |
| maintainer, which hints to a very good Emacs integration, and indeed | |
| [[https://www.ledger-cli.org/3.0/doc/ledger-mode.html][=ledger-mode=]] offers a great environment for editing, reporting and modifying | |
| Ledger files. But even though it is a pretty complete major mode, I missed being | |
| able to organize it my way. Sure you can use =outline-minor-mode= or even | |
| [[https://github.com/alphapapa/outshine/][=outshine=]], but I wanted to have it in Org-mode. | |
| I should also note that, from now on, I'll be referring to editing a source | |
| block in a buffer that has all of the content of the final tangled file as | |
| editing a source block /with context/. I actually named it | |
| =org-contextual-src-mode=. | |
| Keep in mind that the title says "Literate *Ledger* Programming(?)", but this is | |
| (probably) reproducible with any language that you desire. It's just that I use | |
| it mainly for Ledger and, at the point of writing this, have only seen real use | |
| for Ledger. | |
| * Why not just have it as an Org file? | |
| Well, because there is some value in using =ledger-mode= (and, thinking about it | |
| a little, in other modes as well) with the context of the full file when editing | |
| it. In =ledger-mode= one can, for example, generate reports and get the balance | |
| for a single account, as well as having completion based on your previous | |
| postings and whatnot. But mostly I guess its because I've always wanted to be | |
| able to edit a part of a bigger file in Org-mode, and this was the turning point | |
| for me to take a jab at it. | |
| I like to have Org-mode help me organize things in my life, and I am having more | |
| and more of a tendency to do all things in Emacs and from the things I do in | |
| Emacs, the more I can integrate with Org-mode the better. I have also wrote | |
| about how I [[file:20210308020111-contrived_la_tex_templates_for_org_mode.org][have Org-mode manage my templates]] previously, and this is going in | |
| sort of a similar direction. | |
| It might be worth noting that most of the benefits from using what I describe | |
| here can be achieved with setting the variable =ledger-master-file= on something | |
| like =.dir-locals.el=. This variable being set tells ledger to use that file for | |
| reports and balances, instead of the current buffer. | |
| * The basics of the setup a.k.a. tl;dr | |
| I'll try to be brief. I overrode =org-edit-src-code= with an =:around= advice | |
| that tests if the current element is a source block[fn:1], which is necessary | |
| since =org-edit-src-code= is also used for example blocks, and if the language | |
| of the source block is in =org-contextual-src-langs= which is a variable that | |
| holds a list of languages that should consider the context of the whole tangled | |
| file when editing. In particular, ="ledger"= is a member of this list, so is | |
| ="latex"= in my particular case. | |
| Then, after that's established, I tangle all source blocks related to the file | |
| of the current block, if any (if none, just call the usual =org-edit-src-code=), | |
| and then create a new buffer with the contents of the tangled file, but narrowed | |
| to the result of searching the current block's content in the file. The tangling | |
| is necessary since you might be in the middle of editing the block and haven't | |
| tangled yet, in which case the resulting file is incomplete and the search will | |
| fail. | |
| ** How to use it | |
| It is pretty simple: | |
| 1. Add the file =org-contextual-src.el= to your =load-path= and call ~(require | |
| 'org-contextual-src)~. | |
| 2. Set the variable =org-contextual-src-langs= to the languages you would like | |
| to have the context when editing (by default it is set to ~'("ledger" | |
| "latex")~). | |
| 3. Call =org-contextual-src-mode= in an Org buffer. The minor mode is | |
| buffer-local, so you would call it only on the buffers you would like to have | |
| this behavior. Although, if you always want it, it can be achieved with | |
| ~(add-hook 'org-mode-hook #'org-contextual-src-mode)~. | |
| * Let's get into details | |
| I'll try to explain my thought process so that you could replicate what I have | |
| achieved in your own way, if you wish to do so. I'll also be providing a [[https://gist.github.com/bigodel/15599f3c1da23d1008b7d7d4ff8bff77][gist]] | |
| with this document in Org-mode and the Emacs Lisp file with the whole code. | |
| As a first step, I took a look at what function is being called when issuing the | |
| keybinding =C-c '= in a source block. Turns out it is =org-edit-special=, which | |
| is a catch-all function to edit all sorts of blocks and structures within | |
| Org-mode. From that, I looked through the definition to figure out the function | |
| responsible for editing source blocks, and it so happens that it is | |
| =org-edit-src-code=. This function takes care of editing source blocks and | |
| example blocks, but I am only interested in the source block part. | |
| ** Wrapping =org-edit-src-code= | |
| In my =org-contextual-edit-src= (the name I gave for the wrapper of | |
| =org-edit-src-code=), I mimmick most of the behavior of the original function. | |
| One minor difference is that the first =let=-bind [[(tangle)][gets the information for the | |
| =:tangle= header argument]] and [[(file)][tries to resolve the file]] | |
| #+caption: The new variables introduced to =org-edit-src-code=. | |
| #+begin_src emacs-lisp | |
| ;; (let (... | |
| (block-begin (and babel-info (nth 5 babel-info))) | |
| (tangle (and babel-info (cdr (assq :tangle (nth 2 babel-info))))) ; (ref:tangle) | |
| (file (cond ; (ref:file) | |
| ((string= tangle "yes") | |
| (expand-file-name | |
| (concat (file-name-sans-extension (buffer-name)) "." | |
| (or (cdr (assoc lang org-babel-tangle-lang-exts)) | |
| lang)))) | |
| ((string= tangle "no") nil) | |
| ((> (length tangle) 0) | |
| (expand-file-name tangle)))) | |
| ;; ... | |
| #+end_src | |
| The big difference comes when a few conditions are met, in order (at the time of | |
| writing this), but not limited to: | |
| - The argument =CODE= is nil. | |
| This argument is only used when calling =org-babel-expand-src-block=. In this | |
| function, the contents of the source block are expanded (noweb references, | |
| etc.) and they are passed to =org-edit-src-code= to be edited. Since we won't | |
| be doing any editing in this case, just call the original function. | |
| - The current block is a *source* block. | |
| As mentioned previously, =org-edit-src-code= also handles example blocks, but | |
| we are only interested in source blocks. | |
| - The language of the current block is a member of =org-contextual-src-langs=. | |
| This variable consists of a list of strings, which in turn are Org Babel | |
| languages, for which I would like to have contextual editing. I do so because | |
| if in a same file I have some Emacs Lisp source blocks and some Ledger source | |
| blocks, I wouldn't want to have contextual editing for the Emacs Lisp blocks, | |
| but would do for the Ledger blocks, so enabling =org-contextual-src-mode= | |
| would give me the desired behavior. | |
| - The header argument =:noweb= is =no=. | |
| I also don't want to edit blocks that have noweb references with their | |
| context. I rely on searching for the block's contents in the tangled file in | |
| order to narrow the editing buffer to the correct place, so editing a block-- | |
| with its context--that has a noweb reference, would require me to either | |
| expand the reference, in which case the final result that should be written to | |
| the source block is going to be the full expanded reference, which goes in the | |
| opposite direction of the intentions of noweb references. | |
| - If a file to be tangled has been identified. | |
| This is done either through the header argument [[https://orgmode.org/manual/Extracting-Source-Code.html#Header-arguments][=:tangle=]] set to either =yes= | |
| or a filename, or by giving an ID to the source block ([[https://orgmode.org/manual/Structure-of-Code-Blocks.html][=#+name=]] property or | |
| [[https://orgmode.org/manual/Noweb-Reference-Syntax.html][=:noweb-ref=]] header argument.) I'll get into a bit more detail [[#file-from-noweb][later]]. | |
| If all of these are met, we then tangle all of the blocks for the tangle file of | |
| the block at point. | |
| Now comes the most relevant part. | |
| *** Where the "magic" happens | |
| This is the point where I need to tangle the file. We only ACTUALLY need to | |
| tangle if the file was modified, because since we are going to use the contents | |
| of the source block to search on the tangled file, that means that if the | |
| current Org buffer is modified (checked with ~(buffer-modified-p)~), the | |
| resulting tangled file is probably out of date, so update it. | |
| #+caption: Code relevant to the tangling. | |
| #+begin_src emacs-lisp | |
| ;; tangle only the file associated with the current block if the buffer | |
| ;; was not saved | |
| (when (buffer-modified-p) | |
| (save-excursion | |
| (goto-char block-begin) | |
| (org-babel-tangle '(16)))) | |
| #+end_src | |
| Ok so what is really the difference between the usual approach to editing source | |
| blocks? It's actually pretty simple. We save the value of the buffer name for | |
| the editing buffer, which is just like the original =org-edit-src-code=, the | |
| contents of the tangled file and the contents of the source block (note here | |
| that the original function has an =and= to check if CODE was given, but we don't | |
| need to do that because of the first point [[#wrapping-org-edit-src-code][above]]) in variables on a =let*=-bind. | |
| I store them here because they are used as arguments to =org-src--edit-element=, | |
| but I need some of them after the execution of this function, so I found it | |
| easier to just store them on a =let= and use the values after the execution. | |
| #+caption: Values that I need outside =org-src--edit-element=. | |
| #+begin_src emacs-lisp | |
| (let* ((buffer-name (or edit-buffer-name | |
| (org-src--construct-edit-buffer-name | |
| (buffer-name) lang))) | |
| ;; the contents of the tangled file (if we got to here, we have it) | |
| (file-contents (with-temp-buffer | |
| (insert-file-contents file) | |
| (buffer-substring-no-properties | |
| (point-min) (point-max)))) | |
| ;; the actual contents of the source block. | |
| (contents (nth 2 (org-src--contents-area elt)))) | |
| ;; (org-src--edit-element ...) | |
| #+end_src | |
| Inside this =let=, the first thing we do is call =org-src--edit-element=. The | |
| call is very similar to how =org-edit-src-code= does it, the only differences | |
| being that =buffer-name= is used instead of the =or=, since we saved that in a | |
| variable, and the contents we give this function is the contents of the tangled | |
| file (=file-contents=). | |
| After the execution of =org-src--edit-element=, our cursor is (supposedly) on | |
| the newly generated edit buffer, with the appropriate name and buffer-local | |
| variables, and the contents of the whole file; now all that is left is for us to | |
| narrow to the contents of the block: | |
| #+caption: The "magic". | |
| #+begin_src emacs-lisp | |
| ;; narrow to the relevant part | |
| (with-current-buffer buffer-name | |
| (let* ((contents-end (search-forward contents)) | |
| (contents-beg (search-backward contents))) | |
| (narrow-to-region contents-beg contents-end) | |
| ;; enable it in the editing buffer ; (ref:enable-mode) | |
| (org-contextual-src-mode t))) | |
| #+end_src | |
| As a small side note, I [[(enable-mode)][activate the mode in the editing buffer]] because I use | |
| the fact that this is a contextual editing buffer in other parts of it. | |
| *** Checking for a file in other blocks | |
| :PROPERTIES: | |
| :CUSTOM_ID: file-from-noweb | |
| :END: | |
| One caveat I found when first implementing this is that if I had, for example, a | |
| single source block with a bunch of noweb references to other blocks in other | |
| places, if I tried to edit one of these blocks with its context, it would fail, | |
| and that forced me to place the blocks in the order in which I wished they would | |
| appear in the file. But, since this is Emacs and we're dealing with Emacs Lisp, | |
| of course there is a workaround. | |
| Before[fn:2] checking [[#wrapping-org-edit-src-code][these points]], I check if the file is null, e.g., I | |
| couldn't figure out the file to be tangled from current source block. When that | |
| is the case, I have a function that [[(name-or-ref)][gets the name or noweb reference]] for the | |
| current block, and searchs for =<<name-or-ref>>=. For each match, check if the | |
| we are currently on a source block (it could be an internal link) and if so, | |
| [[(file-noweb)][extract the file to be tangled]]. Yeah, this function retrieves the file from the | |
| *first* match of a source block, but I couldn't figure out how to do it in | |
| general, and this seems like it would cover 99.5% of the cases. | |
| #+caption: Getting the file from a noweb reference. | |
| #+begin_src emacs-lisp | |
| (let* (;; ... | |
| (name-or-ref (or (and (not (string= "" name)) name) ; (ref:name-or-ref) | |
| ;; or it has a noweb-ref (name has precedence) | |
| (cdr (assq :noweb-ref (nth 2 babel-info))))) | |
| ;; ... | |
| ) | |
| ;; ... | |
| (when (not (string= "no" match-tangle)) | |
| (setq file (expand-file-name ; (ref:file-noweb) | |
| (cond | |
| ((string= match-tangle "yes") | |
| (concat | |
| (file-name-sans-extension (buffer-name)) "." | |
| (or (cdr (assoc lang org-babel-tangle-lang-exts)) | |
| lang))) | |
| ((> (length match-tangle) 0) match-tangle)) | |
| block-begin (nth 5 babel-info)))) | |
| (list file block-begin)) | |
| #+end_src | |
| ** A few modifications on other functions | |
| Some other functions had to be advised for it to work. One of those functions | |
| was =org-edit-src-exit=. This function is what is usually bound to =C-c '= in | |
| the dedicated editing buffer, and is responsible for finishing the editing | |
| session, while writing the edited content back on the source block in the Org | |
| file. It doesn't differ much from the original function, but we have to add an | |
| advice arround it, so we need to check when to use our modified version, and | |
| when to use the original functoh, like so | |
| <<advice-check>> | |
| #+caption: Function to be wrapped around =org-edit-src-exit=. | |
| #+begin_src emacs-lisp | |
| (defun org-contextual-src-edit-exit (orig-fun &rest args) | |
| (if (or org-contextual-src--no-context-p | |
| (not org-contextual-src-mode)) | |
| (apply orig-fun args) | |
| ;; ... | |
| #+end_src | |
| I only had to change two little things on my custom function: 1. the variable | |
| =coordinates= is populated with =org-contextual-src--coordinates=, which gets | |
| the coordinates in which to put the cursor at the Org buffer, in relation to the | |
| a beginning and end position, which are, in the original =org-edit-src-exit=, 1 | |
| as the beginning position, which corresponds to the beginning of the buffer, | |
| regardless of narrowing, and =(point-max)=, which is the position of the end of | |
| the buffer; however, in our version we would like to consider only the text | |
| inside the narrowing, so we simply substitute the 1 for =(point-min)=, and the | |
| =org-contextual-src--coordinates= behaves just like =org-src--coordinates= but | |
| without wrapping it with a =org-with-wide-buffer=; and 2. set the variable | |
| =org-contextual-src--no-context-p= to =nil=. More about this variable [[before-advice][below]]. | |
| Another one of those functions was =org-src--contents-for-write-back=, which is | |
| a function that returns the contents that should be written back to the block. | |
| This function had to be advised because it was getting the contents of the whole | |
| buffer, unwidened, which would mean that the whole final file would be written | |
| back to the block. That is obviously not what we want, we want to get the | |
| contents of the *visible* part of the buffer, since we have it already narrowed. | |
| I also had to check if we are using the context or not just like [[advice-check][here]]. The rest | |
| of the function is exactly like the original =org-src--contents-for-write-back=, | |
| with the exception that the variable =contents= is initialized with | |
| ~(buffer-string)~ instead of ~(org-with-wide-buffer (buffer-string))~, similarly | |
| to =org-contextual-src--coordinates=. It is used when either saving or exiting, | |
| that's why we couldn't change the behavior of it only in the case that we are | |
| calling =org-contextual-src-edit-exit= in a buffer that is considering the | |
| context, like we did with =org-contextual-src--coordinates=. | |
| <<before-advice>> | |
| There is only one other function which I have advised, but this one is much | |
| simpler. When you issue a command inside a source block, Org actually executes | |
| it in a dedicated edit buffer, just like if you have called =org-edit-src-code=, | |
| but invisibly. Because of that, we have to override this behavior in case we are | |
| on a block that should be executed with the context. That is because, if | |
| everytime we issue a command in a source block we have to actually tangle it, | |
| given that we are going to call our =org-contextual-edit-src=, which tangles all | |
| blocks related to the final file, it would be really annoying to edit any source | |
| block in the Org file itself, but sometimes for quick edits its just easier to | |
| change it right there. The function that does this, | |
| =org-babel-do-key-sequence-in-edit-buffer=, has a =:before= advice that sets | |
| =org-contextual-src--no-context-p= to =t=, that's why all of the functions that | |
| are advices check if the mode is on, or if this variable is true. | |
| ** Defining the minor mode | |
| In order to make it easier to activate this behavior on specific buffers, I | |
| gathered the advicing of the functions in a buffer-local minor mode. The body of | |
| the minor mode is: | |
| #+begin_src emacs-lisp | |
| (if org-contextual-src-mode | |
| (progn | |
| (advice-add 'org-edit-src-code :around #'org-contextual-src-edit-code) | |
| (advice-add 'org-edit-src-exit :around #'org-contextual-src-edit-exit) | |
| (advice-add 'org-src--contents-for-write-back | |
| :around #'org-contextual-src--contents-for-write-back) | |
| (advice-add 'org-babel-do-key-sequence-in-edit-buffer | |
| :before | |
| #'org-contextual-src-do-key-sequence-in-edit-buffer-advice) | |
| (dolist (func org-contextual-src--narrow-functions) ; (ref:disable-narrow) | |
| (advice-add `,func :around #'org-contextual-src-narrow-advice))) | |
| (advice-remove 'org-edit-src-code #'org-contextual-edit-src) | |
| (advice-remove 'org-edit-src-exit #'org-contextual-edit-src-exit) | |
| (advice-remove 'org-src--contents-for-write-back | |
| #'org-src--contextual-contents-for-write-back) | |
| (advice-remove 'org-babel-do-key-sequence-in-edit-buffer | |
| #'org-babel-do-key-sequence-in-edit-buffer-ad) | |
| (dolist (func org-contextual-src--narrow-functions) | |
| (advice-add `,func :around #'org-contextual-src-narrow-advice))) | |
| #+end_src | |
| <<disable-narrowing>> | |
| I guess I should mention that I also [[(disable-narrow)][disable any narrowing function]] inside the | |
| contextual editing buffer, because that would mess up how we get the contents to | |
| be written in the block with our =org-contextual-src--contents-for-write-back=. | |
| And done! | |
| * Conclusion and caveats | |
| And this is basically it. I know it might have been a lot to digest, and I | |
| recognize that I may be wordy most of the time, but I wanted to share this | |
| little hack with the world because I feel that many would like to keep their | |
| =ledger= journal in Org-mode but not being able to access the context of the | |
| whole file have turned them away. | |
| There are some caveats with it, though. The biggest one is that if these | |
| functions ever get changed I would have to reflect the change and adapt the | |
| modified code to work as I have intended, though I don't think this would be | |
| much of a problem since this part of Org-mode seems stable enough. Another one | |
| of those is that one cannot call any of the narrowing functions, as mentioned | |
| [[disable-narrowing][above]]. Another one is that it takes a couple of milliseconds before the editing | |
| buffer shows up because of the tangling. But despite them, I am enjoying very | |
| much having sort of "the best of both worlds." | |
| One minor issue that I haven't yet solved, and don't know what is causing it, is | |
| that calling =save-buffer= saves the entire file to the source block, instead of | |
| just the visible contents of the buffer. In the editing buffer, =C-x C-s= gets | |
| bound to =org-edit-src-save=, which works as desired without having to advise or | |
| anything like that (because we've already advised | |
| =org-src--contents-for-write-back=), but calling =save-buffer= behaves like I | |
| mentioned, which might not be ideal. | |
| Ps.: I know that one shouldn't [[https://karl-voit.at/2020/10/23/avoid-web-forums/][contribute anything relevant in web forums like | |
| Reddit]], but I haven't had the time to setup my blog yet so I'm just posting here | |
| for now. | |
| * Footnotes | |
| [fn:2] I check it before checking the points becuase one of the checks I didn't | |
| mention is if the file is null, in which case no tangling is happening and we | |
| should just use the original function. | |
| [fn:1] I don't think it would be beneficial to have it working with inline | |
| source blocks, but that can be arranged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;;; org-contextual-src.el --- Edit SRC block with context in Org-mode -*- lexical-bindings: t; -*- | |
| ;; Author: João Pedro de Amorim Paula <jpedrodeamorim@gmail.com> | |
| ;;; Commentary: | |
| ;; | |
| ;; This is basically a hack around `org-edit-src-code' that tangles all the | |
| ;; blocks for the tangle file of the block at point and then proceeds to create | |
| ;; a buffer with a copy of the tangled file so that we can edit this buffer, | |
| ;; instead of the real file, narrowed to the block we were interested in. this | |
| ;; allows us to have all of the context of the final file, while still editing | |
| ;; it as it were just a simple code block in org-mode. | |
| ;; | |
| ;;; Code: | |
| (require 'ob-tangle) | |
| (require 'org-element) | |
| (require 'org-src) | |
| (defvar org-contextual-src-langs '("ledger" "latex") | |
| "List of Babel languages that should be edited with the context.") | |
| (defvar org-contextual-src--no-context-p nil | |
| "Variable to store if we should use or not the context.") | |
| ;; silence byte-compiler | |
| (defvar org-contextual-src-mode) | |
| (defun org-contextual-src-edit-code (orig-fun &optional code edit-buffer-name) | |
| "Edit the source block at point with tangle context. | |
| This is meant to be used as an :around advice on | |
| `org-edit-src-code', so ORIG-FUN is precisely the symbol for the | |
| function, and CODE and EDIT-BUFFER-NAME are the arguments for | |
| `org-edit-src-code'. | |
| If the block has a NAME property or :noweb-ref header arguments, | |
| and has no :tangle header argument, i.e., couldn't determine the | |
| file to be tangled, try to look for a source block that tangles | |
| the current block and tangle the first block that matches the | |
| search and which :tangle is anything other than 'no'." | |
| (interactive) | |
| (let* ((elt (org-element-at-point)) | |
| (type (car elt)) | |
| (lang (when (eq type 'src-block) | |
| (org-element-property :language elt))) | |
| (lang-f (and (eq type 'src-block) (org-src-get-lang-mode lang))) | |
| (babel-info (and (eq type 'src-block) | |
| (org-babel-get-src-block-info 'light))) | |
| (block-begin (and babel-info (nth 5 babel-info))) | |
| (tangle (and babel-info (cdr (assq :tangle (nth 2 babel-info))))) | |
| (file (cond | |
| ((string= tangle "yes") | |
| (expand-file-name | |
| (concat (file-name-sans-extension (buffer-name)) "." | |
| (or (cdr (assoc lang org-babel-tangle-lang-exts)) | |
| lang)))) | |
| ((string= tangle "no") nil) | |
| ((> (length tangle) 0) | |
| (expand-file-name tangle))))) | |
| ;; only try to follow a noweb reference if we couldn't figure out the | |
| ;; file to be tangled | |
| (unless file | |
| (let ((file-alist (org-contextual-src--file-from-noweb babel-info))) | |
| (setq file (car file-alist) | |
| block-begin (cadr file-alist)))) | |
| ;; don't edit with context if any of these conditions are met | |
| (if (or org-contextual-src--no-context-p | |
| (not org-contextual-src-mode) ; the mode is not on | |
| code ; if CODE is not nil, don't bother | |
| (not (eq type 'src-block)) | |
| (not (member lang org-contextual-src-langs)) | |
| ;; doesn't really work with noweb, so this return t if :noweb is | |
| ;; not nil or is something other than | |
| (not (string= "no" (cdr (assq :noweb (nth 2 babel-info))))) | |
| (null file)) | |
| (apply orig-fun `(,code ,edit-buffer-name)) | |
| ;; tangle only the file associated with the current block if the buffer | |
| ;; was not saved | |
| (when (buffer-modified-p) | |
| (save-excursion | |
| (goto-char block-begin) | |
| (org-babel-tangle '(16)))) | |
| ;; store some values we want to use after | |
| (let* ((buffer-name (or edit-buffer-name | |
| (org-src--construct-edit-buffer-name | |
| (buffer-name) lang))) | |
| ;; the contents of the tangled file (if we got to here, we have it) | |
| (file-contents (with-temp-buffer | |
| (insert-file-contents file) | |
| (buffer-substring-no-properties | |
| (point-min) (point-max)))) | |
| ;; the actual contents of the source block. | |
| (contents (nth 2 (org-src--contents-area elt)))) | |
| ;; open the edit buffer | |
| (org-src--edit-element elt buffer-name lang-f | |
| (and (null code) | |
| (lambda () (org-escape-code-in-region | |
| (point-min) (point-max)))) | |
| ;; the contents of the buffer should be the | |
| ;; contents of the file | |
| file-contents) | |
| ;; narrow to the relevant part | |
| (with-current-buffer buffer-name | |
| (let* ((contents-end (search-forward contents)) | |
| (contents-beg (search-backward contents))) | |
| (narrow-to-region contents-beg contents-end) | |
| ;; enable it in the editing buffer | |
| (org-contextual-src-mode t)))) | |
| ;; finalize buffer. | |
| (setq-local org-coderef-label-format | |
| (or (org-element-property :label-fmt elt) | |
| org-coderef-label-format)) | |
| (when (eq type 'src-block) | |
| (setq org-src--babel-info babel-info) | |
| (let ((edit-prep-func (intern (concat "org-babel-edit-prep:" lang)))) | |
| (when (fboundp edit-prep-func) | |
| (funcall edit-prep-func babel-info)))) | |
| t))) | |
| (defun org-contextual-src--file-from-noweb (babel-info) | |
| "Get the tangle file for current block. | |
| Search for the first match of the noweb reference, which is | |
| either the value of the header argument :noweb-ref or the NAME | |
| for the current block, and return the file it is supposed to | |
| tangle to. BABEL-INFO is the result of | |
| `org-babel-get-src-block-info'. | |
| The return value of this function is an alist whose `car' is the | |
| file name and `cdr' is the position of the first match." | |
| (let* ((lang (nth 0 babel-info)) | |
| (name (nth 4 babel-info)) | |
| ;; block has a name so return it | |
| (name-or-ref (or (and (not (string= "" name)) name) | |
| ;; or it has a noweb-ref (name has precedence) | |
| (cdr (assq :noweb-ref (nth 2 babel-info))))) | |
| file block-begin match-pos match-info match-tangle) | |
| (when name-or-ref | |
| (save-excursion | |
| (goto-char (point-min)) | |
| (while (null file) | |
| (setq match-pos (search-forward | |
| (org-babel-noweb-wrap name-or-ref) nil 'noerror) | |
| match-info (and (eq (car (org-element-at-point)) 'src-block) | |
| (org-babel-get-src-block-info 'light)) | |
| match-tangle (cdr (assq :tangle (nth 2 match-info)))) | |
| ;; we found a use of the reference of the block, now we need | |
| ;; to check if it is supposed to tangle or not | |
| (when (not (string= "no" match-tangle)) | |
| (setq file (expand-file-name | |
| (cond | |
| ((string= match-tangle "yes") | |
| (concat | |
| (file-name-sans-extension (buffer-name)) "." | |
| (or (cdr (assoc lang org-babel-tangle-lang-exts)) | |
| lang))) | |
| ((> (length match-tangle) 0) match-tangle))) | |
| block-begin (nth 5 match-info)))))) | |
| (list file block-begin))) | |
| (defun org-contextual-src--contents-for-write-back (orig-fun &rest args) | |
| "Like `org-src--contents-for-write-back' but be context aware. | |
| This is meant to be used as an :around advice on | |
| `org-src--contents-for-write-back', so that's why ORIG-FUN and | |
| ARGS are its arguments. | |
| The only difference compared to | |
| `org-src--contents-for-write-back' is that the variable | |
| 'contents' is just `buffer-string', without | |
| `org-with-wide-buffer'." | |
| (interactive) | |
| (if (or org-contextual-src--no-context-p | |
| (not org-contextual-src-mode)) | |
| (apply orig-fun args) | |
| (let ((indentation-offset | |
| (if org-src--preserve-indentation 0 | |
| (+ (or org-src--block-indentation 0) | |
| (if (equal org-src--source-type 'src-block) | |
| org-src--content-indentation | |
| 0)))) | |
| (use-tabs? (and (> org-src--tab-width 0) t)) | |
| (source-tab-width org-src--tab-width) | |
| (contents (buffer-string)) | |
| (write-back org-src--allow-write-back)) | |
| (with-temp-buffer | |
| ;; reproduce indentation parameters from source buffer. | |
| (setq indent-tabs-mode use-tabs?) | |
| (when (> source-tab-width 0) (setq tab-width source-tab-width)) | |
| ;; apply WRITE-BACK function on edit buffer contents. | |
| (insert (org-no-properties contents)) | |
| ;; we sometimes need to add 1 newline because we have a part of | |
| ;; buffer, and not a complete buffer with the newline that Emacs adds | |
| (unless (and (goto-char (point-max)) (bolp) (eolp)) | |
| (newline 1)) | |
| (goto-char (point-min)) | |
| (when (functionp write-back) (save-excursion (funcall write-back))) | |
| ;; add INDENTATION-OFFSET to every non-empty line in buffer, | |
| ;; unless indentation is meant to be preserved. | |
| (when (> indentation-offset 0) | |
| (while (not (eobp)) | |
| (skip-chars-forward " \t") | |
| (unless (eolp) ;ignore blank lines | |
| (let ((i (current-column))) | |
| (delete-region (line-beginning-position) (point)) | |
| (indent-to (+ i indentation-offset)))) | |
| (forward-line))) | |
| (buffer-string))))) | |
| (defun org-contextual-src--coordinates (pos beg end) | |
| "Like `org-src--coordinates' but don't widen. | |
| POS, BEG and END are the same as in `org-src--coordinates'." | |
| (if (>= pos end) 'end | |
| (goto-char (max beg pos)) | |
| (cons (count-lines beg (line-beginning-position)) | |
| ;; column is relative to the end of line to avoid problems of | |
| ;; comma escaping or colons appended in front of the line. | |
| (- (current-column) | |
| (progn (end-of-line) (current-column)))))) | |
| (defun org-contextual-src-edit-exit (orig-fun &rest args) | |
| "Like `org-edit-src-exit' but context aware. | |
| This is meant to be used as an :around advice on | |
| `org-edit-src-exit', so that's why ORIG-FUN and ARGS are its | |
| arguments. | |
| The only difference compared to `org-edit-src-exit', is that the | |
| variable 'coordinates' is the value of calling | |
| `org-contextual-src--coordinates', instead of | |
| `org-src--coordinates', with `point-min' instead of 1." | |
| (interactive) | |
| (if (or org-contextual-src--no-context-p | |
| (not org-contextual-src-mode)) | |
| (apply orig-fun args) | |
| (unless (org-src-edit-buffer-p) (error "Not in a sub-editing buffer")) | |
| (let* ((beg org-src--beg-marker) | |
| (end org-src--end-marker) | |
| (write-back org-src--allow-write-back) | |
| (remote org-src--remote) | |
| (coordinates (and (not remote) | |
| (org-contextual-src--coordinates | |
| (point) (point-min) (point-max)))) | |
| (code (and write-back (org-src--contents-for-write-back)))) | |
| (set-buffer-modified-p nil) | |
| ;; switch to source buffer. kill sub-editing buffer. | |
| (let ((edit-buffer (current-buffer)) | |
| (source-buffer (marker-buffer beg))) | |
| (unless source-buffer (error "Source buffer disappeared. Aborting")) | |
| (org-src-switch-to-buffer source-buffer 'exit) | |
| (kill-buffer edit-buffer)) | |
| ;; insert modified code. ensure it ends with a newline character. | |
| (org-with-wide-buffer | |
| (when (and write-back (not (equal (buffer-substring beg end) code))) | |
| (undo-boundary) | |
| (delete-region beg end) | |
| (let ((expecting-bol (bolp))) | |
| (insert code) | |
| (when (and expecting-bol (not (bolp))) (insert "\n"))))) | |
| ;; if we are to return to source buffer, put point at an | |
| ;; appropriate location. in particular, if block is hidden, move | |
| ;; to the beginning of the block opening line. | |
| (unless remote | |
| (goto-char beg) | |
| (cond | |
| ;; block is hidden; move at start of block. | |
| ((cl-some (lambda (o) (eq (overlay-get o 'invisible) 'org-hide-block)) | |
| (overlays-at (point))) | |
| (beginning-of-line 0)) | |
| (write-back (org-src--goto-coordinates coordinates beg end)))) | |
| ;; clean up left-over markers and restore window configuration. | |
| (set-marker beg nil) | |
| (set-marker end nil) | |
| (when org-src--saved-temp-window-config | |
| (unwind-protect | |
| (set-window-configuration org-src--saved-temp-window-config) | |
| (setq org-src--saved-temp-window-config nil))))) | |
| ;; reset the value of this variable regardless of contextual editing or not | |
| (setq org-contextual-src--no-context-p nil)) | |
| (defun org-contextual-src-do-key-sequence-in-edit-buffer-advice (&rest _) | |
| "Set `org-contextual-src--no-context-p' to t. | |
| This is meant to be used as a :before advice on | |
| `org-babel-do-key-sequence-in-edit-buffer'. If this function was | |
| called, that means that we don't want to execute the key sequence | |
| in an edit buffer, since that would imply that we would tangle | |
| and all of that." | |
| (setq org-contextual-src--no-context-p t)) | |
| (defun org-contextual-src-narrow-advice (orig-fun &rest args) | |
| "Function to be used as :around advice on narrowing functions. | |
| This function is supposed to wrap `narrow-to-region', | |
| `narrow-to-defun', `narrow-to-page' and `widen'. This is supposed | |
| to check if the current buffer is a source editing buffer and if | |
| `org-contextual-src-mode' is non-nil. On the contrary, just call | |
| the usual function. ORIG-FUN and ARGS are what is necessary to | |
| call the original function." | |
| (if (and (fboundp 'org-src-edit-buffer-p) | |
| (org-src-edit-buffer-p) | |
| org-contextual-src-mode) | |
| (user-error "Narrowing or widening not allowed in contextual editing") | |
| (apply orig-fun args))) | |
| (defvar org-contextual-src--narrow-functions | |
| '(narrow-to-defun narrow-to-page narrow-to-region widen) | |
| "Narrowing functions that should be advised. | |
| Symbols in this list are expected to be functions and won't work | |
| on editing buffer for contextual editing.") | |
| ;;;###autoload | |
| (define-minor-mode org-contextual-src-mode | |
| "Edit Org-mode files in a separate buffer with all tangled blocks. | |
| What this means, is that if you're editing a source block in | |
| Org-mode that is going to be tangled, when using | |
| `org-edit-special', if the block's language is in | |
| `org-contextual-src-langs', then the editing buffer is actually | |
| the whole tangled file narrowed, using `narrow-to-region', to the | |
| relevant part." | |
| :global nil | |
| :lighter " OrgCtxSrc" | |
| (if org-contextual-src-mode | |
| (progn | |
| (advice-add 'org-edit-src-code :around #'org-contextual-src-edit-code) | |
| (advice-add 'org-edit-src-exit :around #'org-contextual-src-edit-exit) | |
| (advice-add 'org-src--contents-for-write-back | |
| :around #'org-contextual-src--contents-for-write-back) | |
| (advice-add 'org-babel-do-key-sequence-in-edit-buffer | |
| :before | |
| #'org-contextual-src-do-key-sequence-in-edit-buffer-advice) | |
| (dolist (func org-contextual-src--narrow-functions) | |
| (advice-add `,func :around #'org-contextual-src-narrow-advice))) | |
| (advice-remove 'org-edit-src-code #'org-contextual-edit-src) | |
| (advice-remove 'org-edit-src-exit #'org-contextual-edit-src-exit) | |
| (advice-remove 'org-src--contents-for-write-back | |
| #'org-src--contextual-contents-for-write-back) | |
| (advice-remove 'org-babel-do-key-sequence-in-edit-buffer | |
| #'org-babel-do-key-sequence-in-edit-buffer-ad) | |
| (dolist (func org-contextual-src--narrow-functions) | |
| (advice-add `,func :around #'org-contextual-src-narrow-advice)))) | |
| (provide 'org-contextual-src) | |
| ;;; org-contextual-src.el ends here |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment